<?php

namespace Lvzmen\Helper;

use http\Exception\InvalidArgumentException;

class iArrayHelper
{
    /**
     * 对数组进行统计
     *
     * example:
     * $arr = [
     *      ['col1' => 'a', 'col2' => 'b', 'col3' => 'c', 's1' => 1, 's2' => 2,]
     *      ['col1' => 'a', 'col2' => 'b', 'col3' => 'cc', 's1' => 1, 's2' => 2,]
     *      ['col1' => 'aa', 'col2' => 'bb', 'col3' => 'cc', 's1' => 1, 's2' => 2,]
     * ]
     * sum($arr, ['col1' => 'n1', 'col2' => 'n2'], ['s1' => 'r1', 's2' => 'r2']);
     * result:
     * [
     *      ['n1' => 'a', 'n2' => 'b', 'r1' => 2, 'r2' => 4],
     *      ['n1' => 'aa', 'n2' => 'bb', 'r1' => 1, 'r2' => 2],
     * ]
     *
     *
     * @param array $arr
     * @param array $groupBy
     */
    public static function sum(array $arr, array $groupBy, array $columns)
    {
        $result = [];
        foreach ($arr as $item) {
            $key = "";
            foreach ($groupBy as $gk => $tmp) {
                $key .= $item[$gk];
            }
            if (!isset($result[$key])) {
                foreach ($groupBy as $gk => $tmp) {
                    $result[$key][$tmp] = $item[$gk];
                }
                foreach ($columns as $ck => $column) {
                    $result[$key][$column] = $item[$ck];
                }
            } else {
                foreach ($columns as $ck => $column) {
                    $result[$key][$column] += $item[$ck];
                }
            }
        }
        return array_values(array_map(function ($item) use ($columns) {
            foreach ($columns as $ck => $column) {
                $item[$column] = number_format($item[$column], 2, '.', '') * 1;
            }
            return $item;
        }, $result));
    }


    /**
     * 进行字段映射
     * example:
     * $arr = [
     *      ['col1' => 'val1', 'col2' => 'val2']
     * ]
     *
     * self::map($arr, 'col1', 'col2')
     *
     * result:
     * [
     *      ['val1' => 'val2']
     * ]
     *
     * @param array $arr
     * @param string $key
     * @param string $value
     * @return array
     */
    public static function map(array $arr, string $key, string $value)
    {
        $result = [];
        foreach ($arr as $ak => $item) {
            $result[$item[$key]] = $item[$value];
        }

        return $result;
    }

    /**
     * 二位数组排序
     *
     * $arr = [
     *      ['num' => 3],
     *      ['num' => 1],
     *      ['num' => 2],
     * ]
     *
     * self::sort($sort, 'num', 'desc')
     * result:
     *  [
     *      ['num' => 3],
     *      ['num' => 2],
     *      ['num' => 1],
     * ]
     *
     *
     * @param array $arr
     * @param string $column
     * @param string $type
     * @return array
     */
    public static function sort(array $arr, string $column, string $type)
    {
        $type = $type == 'desc' ? SORT_DESC : SORT_ASC;
        array_multisort(array_column($arr, $column), $type, $arr);
        return $arr;
    }

    /**
     * Converts an object or an array of objects into an array.
     * @param object|array|string $object the object to be converted into an array
     * @param array $properties a mapping from object class names to the properties that need to put into the resulting arrays.
     * The properties specified for each class is an array of the following format:
     *
     * ```php
     * [
     *     'app\models\Post' => [
     *         'id',
     *         'title',
     *         // the key name in array result => property name
     *         'createTime' => 'created_at',
     *         // the key name in array result => anonymous function
     *         'length' => function ($post) {
     *             return strlen($post->content);
     *         },
     *     ],
     * ]
     * ```
     *
     * The result of `ArrayHelper::toArray($post, $properties)` could be like the following:
     *
     * ```php
     * [
     *     'id' => 123,
     *     'title' => 'test',
     *     'createTime' => '2013-01-01 12:00AM',
     *     'length' => 301,
     * ]
     * ```
     *
     * @param bool $recursive whether to recursively converts properties which are objects into arrays.
     * @return array the array representation of the object
     */
    public static function toArray($object, $properties = [], $recursive = true)
    {
        if (is_array($object)) {
            if ($recursive) {
                foreach ($object as $key => $value) {
                    if (is_array($value) || is_object($value)) {
                        $object[$key] = static::toArray($value, $properties, true);
                    }
                }
            }
            return $object;
        } elseif (is_object($object)) {
            if (!empty($properties)) {
                $className = get_class($object);
                if (!empty($properties[$className])) {
                    $result = [];
                    foreach ($properties[$className] as $key => $name) {
                        if (is_int($key)) {
                            $result[$name] = $object->$name;
                        } else {
                            $result[$key] = static::getValue($object, $name);
                        }
                    }

                    return $recursive ? static::toArray($result, $properties) : $result;
                }
            }
            $result = [];
            foreach ($object as $key => $value) {
                $result[$key] = $value;
            }
            return $recursive ? static::toArray($result, $properties) : $result;
        }

        return [$object];
    }

    /**
     * Retrieves the value of an array element or object property with the given key or property name.
     * If the key does not exist in the array, the default value will be returned instead.
     * Not used when getting value from an object.
     *
     * The key may be specified in a dot format to retrieve the value of a sub-array or the property
     * of an embedded object. In particular, if the key is `x.y.z`, then the returned value would
     * be `$array['x']['y']['z']` or `$array->x->y->z` (if `$array` is an object). If `$array['x']`
     * or `$array->x` is neither an array nor an object, the default value will be returned.
     * Note that if the array already has an element `x.y.z`, then its value will be returned
     * instead of going through the sub-arrays. So it is better to be done specifying an array of key names
     * like `['x', 'y', 'z']`.
     *
     * Below are some usage examples,
     *
     * ```php
     * // working with array
     * $username = self::getValue($_POST, 'username');
     * // working with object
     * $username = self::getValue($user, 'username');
     * // working with anonymous function
     * $fullName = self::getValue($user, function ($user, $defaultValue) {
     *     return $user->firstName . ' ' . $user->lastName;
     * });
     * // using dot format to retrieve the property of embedded object
     * $street = self::getValue($users, 'address.street');
     * // using an array of keys to retrieve the value
     * $value = self::getValue($versions, ['1.0', 'date']);
     * ```
     *
     * @param array|object $array array or object to extract value from
     * @param string|\Closure|array $key key name of the array element, an array of keys or property name of the object,
     * or an anonymous function returning the value. The anonymous function signature should be:
     * `function($array, $defaultValue)`.
     * The possibility to pass an array of keys is available since version 2.0.4.
     * @param mixed $default the default value to be returned if the specified array key does not exist. Not used when
     * getting value from an object.
     * @return mixed the value of the element if found, default value otherwise
     */
    public static function getValue($array, $key, $default = null)
    {
        if ($key instanceof \Closure) {
            return $key($array, $default);
        }

        if (is_array($key)) {
            $lastKey = array_pop($key);
            foreach ($key as $keyPart) {
                $array = static::getValue($array, $keyPart);
            }
            $key = $lastKey;
        }

        if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) {
            return $array[$key];
        }

        if (($pos = strrpos($key, '.')) !== false) {
            $array = static::getValue($array, substr($key, 0, $pos), $default);
            $key = substr($key, $pos + 1);
        }

        if (is_object($array)) {
            // this is expected to fail if the property does not exist, or __get() is not implemented
            // it is not reliably possible to check whether a property is accessible beforehand
            return $array->$key;
        } elseif (is_array($array)) {
            return (isset($array[$key]) || array_key_exists($key, $array)) ? $array[$key] : $default;
        }

        return $default;
    }


    /**
     * Writes a value into an associative array at the key path specified.
     * If there is no such key path yet, it will be created recursively.
     * If the key exists, it will be overwritten.
     *
     * ```php
     *  $array = [
     *      'key' => [
     *          'in' => [
     *              'val1',
     *              'key' => 'val'
     *          ]
     *      ]
     *  ];
     * ```
     *
     * The result of `ArrayHelper::setValue($array, 'key.in.0', ['arr' => 'val']);` will be the following:
     *
     * ```php
     *  [
     *      'key' => [
     *          'in' => [
     *              ['arr' => 'val'],
     *              'key' => 'val'
     *          ]
     *      ]
     *  ]
     *
     * ```
     *
     * The result of
     * `ArrayHelper::setValue($array, 'key.in', ['arr' => 'val']);` or
     * `ArrayHelper::setValue($array, ['key', 'in'], ['arr' => 'val']);`
     * will be the following:
     *
     * ```php
     *  [
     *      'key' => [
     *          'in' => [
     *              'arr' => 'val'
     *          ]
     *      ]
     *  ]
     * ```
     *
     * @param array $array the array to write the value to
     * @param string|array|null $path the path of where do you want to write a value to `$array`
     * the path can be described by a string when each key should be separated by a dot
     * you can also describe the path as an array of keys
     * if the path is null then `$array` will be assigned the `$value`
     * @param mixed $value the value to be written
     * @since 2.0.13
     */
    public static function setValue(&$array, $path, $value)
    {
        if ($path === null) {
            $array = $value;
            return $array;
        }

        $keys = is_array($path) ? $path : explode('.', $path);

        while (count($keys) > 1) {
            $key = array_shift($keys);
            if (!isset($array[$key])) {
                $array[$key] = [];
            }
            if (!is_array($array[$key])) {
                $array[$key] = [$array[$key]];
            }
            $array = &$array[$key];
        }

        $array[array_shift($keys)] = $value;

        return $array;
    }


    /**
     * 递归移除键值为$key的数组元素
     *
     * $arr = [
     *      'key1' => 'val1',
     *      'key2' => 'val2',
     *      [
     *          'key1' => 'val1'
     *          'key2' => 'val2'
     *      ],
     * ]
     *
     * $result = self::removeByKey($arr, 'key1')
     *
     * 结果为：
     * $arr = [
     *      'key2' => 'val2',
     *      [
     *          'key2' => 'val2'
     *      ],
     * ]
     *
     * $recursive：表示是否对多维数组进行递归处理
     * 举例：
     * $result = self::removeByKey($arr, 'key1', false)
     * 结果为：
     * $arr = [
     *      'key2' => 'val2',
     *      [
     *          'key1' => 'val1' // 不递归，则子元素不处理
     *          'key2' => 'val2'
     *      ],
     * ]
     *
     * @param $array
     * @param $key
     * @param bool $recursive
     * @return array|mixed
     */
    public static function removeByKey($array, $key, $recursive = true)
    {
        if (is_array($array)) {
            foreach ($array as $k => $item) {
                if ($k === $key) {
                    unset($array[$k]);
                    continue;
                }
                if ($recursive && is_array($item)) {
                    $array[$k] = self::removeByKey($item, $key, $recursive);
                }
            }
        }
        return $array;
    }

    /**
     * 递归移除键值为$key的数组元素
     *
     * $arr = [
     *      'key1' => 'val1',
     *      'key2' => 'val2',
     *      [
     *          'key1' => 'val1'
     *          'key2' => 'val2'
     *      ],
     * ]
     *
     * $result = self::removeByValue($arr, ['val1'])
     *
     * 结果为：
     * $arr = [
     *      'key2' => 'val2',
     *      [
     *          'key2' => 'val2'
     *      ],
     * ]
     *
     * $like：表示是否支持模糊搜索
     * 举例：
     * $result = self::removeByKey($arr, ['1'], true)
     * 结果为：
     * $arr = [
     *      'key2' => 'val2',
     *      [
     *          'key2' => 'val2'
     *      ],
     * ]
     *
     * $recursive：表示是否对多维数组进行递归处理
     * 举例：
     * $result = self::removeByKey($arr, ['val1'], false, false)
     * 结果为：
     * $arr = [
     *      'key2' => 'val2',
     *      [
     *          'key1' => 'val1' // 不递归，则子元素不处理
     *          'key2' => 'val2'
     *      ],
     * ]
     *
     * @param array $array
     * @param array $value
     * @param bool $like // 是否启动模糊匹配
     * @param bool $recursive
     * @return array|mixed
     */
    public static function removeByValue($array, $values, $like = false, $recursive = true)
    {
        if (is_array($array)) {
            foreach ($array as $k => $item) {
                if (!is_array($item)) {
                    $check = array_reduce($values, function ($c, $v) use ($item, $like) {
                        return $c || ($like ? (strpos($item, $v) !== false) : ($item === $v));
                    }, false);
                    if ($check) {
                        unset($array[$k]);
                        continue;
                    }
                } elseif ($recursive) {
                    $array[$k] = self::removeByValue($item, $values, $like, $recursive);
                }
            }
        }
        return $array;
    }

    /**
     * Indexes and/or groups the array according to a specified key.
     * The input should be either multidimensional array or an array of objects.
     *
     * The $key can be either a key name of the sub-array, a property name of object, or an anonymous
     * function that must return the value that will be used as a key.
     *
     * $groups is an array of keys, that will be used to group the input array into one or more sub-arrays based
     * on keys specified.
     *
     * If the `$key` is specified as `null` or a value of an element corresponding to the key is `null` in addition
     * to `$groups` not specified then the element is discarded.
     *
     * For example:
     *
     * ```php
     * $array = [
     *     ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
     *     ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
     *     ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
     * ];
     * $result = ArrayHelper::index($array, 'id');
     * ```
     *
     * The result will be an associative array, where the key is the value of `id` attribute
     *
     * ```php
     * [
     *     '123' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
     *     '345' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
     *     // The second element of an original array is overwritten by the last element because of the same id
     * ]
     * ```
     *
     * An anonymous function can be used in the grouping array as well.
     *
     * ```php
     * $result = ArrayHelper::index($array, function ($element) {
     *     return $element['id'];
     * });
     * ```
     *
     * Passing `id` as a third argument will group `$array` by `id`:
     *
     * ```php
     * $result = ArrayHelper::index($array, null, 'id');
     * ```
     *
     * The result will be a multidimensional array grouped by `id` on the first level, by `device` on the second level
     * and indexed by `data` on the third level:
     *
     * ```php
     * [
     *     '123' => [
     *         ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
     *     ],
     *     '345' => [ // all elements with this index are present in the result array
     *         ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
     *         ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
     *     ]
     * ]
     * ```
     *
     * The anonymous function can be used in the array of grouping keys as well:
     *
     * ```php
     * $result = ArrayHelper::index($array, 'data', [function ($element) {
     *     return $element['id'];
     * }, 'device']);
     * ```
     *
     * The result will be a multidimensional array grouped by `id` on the first level, by the `device` on the second one
     * and indexed by the `data` on the third level:
     *
     * ```php
     * [
     *     '123' => [
     *         'laptop' => [
     *             'abc' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
     *         ]
     *     ],
     *     '345' => [
     *         'tablet' => [
     *             'def' => ['id' => '345', 'data' => 'def', 'device' => 'tablet']
     *         ],
     *         'smartphone' => [
     *             'hgi' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
     *         ]
     *     ]
     * ]
     * ```
     *
     * @param array $array the array that needs to be indexed or grouped
     * @param string|\Closure|null $key the column name or anonymous function which result will be used to index the array
     * @param string|string[]|\Closure[]|null $groups the array of keys, that will be used to group the input array
     * by one or more keys. If the $key attribute or its value for the particular element is null and $groups is not
     * defined, the array element will be discarded. Otherwise, if $groups is specified, array element will be added
     * to the result array without any key. This parameter is available since version 2.0.8.
     * @return array the indexed and/or grouped array
     */
    public static function index($array, $key, $groups = [])
    {
        $result = [];
        $groups = (array)$groups;

        foreach ($array as $element) {
            $lastArray = &$result;

            foreach ($groups as $group) {
                $value = static::getValue($element, $group);
                if (!array_key_exists($value, $lastArray)) {
                    $lastArray[$value] = [];
                }
                $lastArray = &$lastArray[$value];
            }

            if ($key === null) {
                if (!empty($groups)) {
                    $lastArray[] = $element;
                }
            } else {
                $value = static::getValue($element, $key);
                if ($value !== null) {
                    if (is_float($value)) {
                        $value = str_replace(',', '.', (string)$value);
                    }
                    $lastArray[$value] = $element;
                }
            }
            unset($lastArray);
        }

        return $result;
    }


    /**
     * Returns the values of a specified column in an array.
     * The input array should be multidimensional or an array of objects.
     *
     * For example,
     *
     * ```php
     * $array = [
     *     ['id' => '123', 'data' => 'abc'],
     *     ['id' => '345', 'data' => 'def'],
     * ];
     * $result = ArrayHelper::getColumn($array, 'id');
     * // the result is: ['123', '345']
     *
     * // using anonymous function
     * $result = ArrayHelper::getColumn($array, function ($element) {
     *     return $element['id'];
     * });
     * ```
     *
     * @param array $array
     * @param int|string|\Closure $name
     * @param bool $keepKeys whether to maintain the array keys. If false, the resulting array
     * will be re-indexed with integers.
     * @return array the list of column values
     */
    public static function getColumn($array, $name, $keepKeys = true)
    {
        $result = [];
        if ($keepKeys) {
            foreach ($array as $k => $element) {
                $result[$k] = static::getValue($element, $name);
            }
        } else {
            foreach ($array as $element) {
                $result[] = static::getValue($element, $name);
            }
        }

        return $result;
    }


    /**
     * Builds a map (key-value pairs) from a multidimensional array or an array of objects.
     * The `$from` and `$to` parameters specify the key names or property names to set up the map.
     * Optionally, one can further group the map according to a grouping field `$group`.
     *
     * For example,
     *
     * ```php
     * $array = [
     *     ['id' => '123', 'name' => 'aaa', 'class' => 'x'],
     *     ['id' => '124', 'name' => 'bbb', 'class' => 'x'],
     *     ['id' => '345', 'name' => 'ccc', 'class' => 'y'],
     * ];
     *
     * $result = ArrayHelper::map($array, 'id', 'name');
     * // the result is:
     * // [
     * //     '123' => 'aaa',
     * //     '124' => 'bbb',
     * //     '345' => 'ccc',
     * // ]
     *
     * $result = ArrayHelper::map($array, 'id', 'name', 'class');
     * // the result is:
     * // [
     * //     'x' => [
     * //         '123' => 'aaa',
     * //         '124' => 'bbb',
     * //     ],
     * //     'y' => [
     * //         '345' => 'ccc',
     * //     ],
     * // ]
     * ```
     *
     * @param array $array
     * @param string|\Closure $from
     * @param string|\Closure $to
     * @param string|\Closure $group
     * @return array
     */
    public static function map2($array, $from, $to, $group = null)
    {
        $result = [];
        foreach ($array as $element) {
            $key = static::getValue($element, $from);
            $value = static::getValue($element, $to);
            if ($group !== null) {
                $result[static::getValue($element, $group)][$key] = $value;
            } else {
                $result[$key] = $value;
            }
        }

        return $result;
    }

    /**
     * Checks if the given array contains the specified key.
     * This method enhances the `array_key_exists()` function by supporting case-insensitive
     * key comparison.
     * @param string $key the key to check
     * @param array $array the array with keys to check
     * @param bool $caseSensitive whether the key comparison should be case-sensitive
     * @return bool whether the array contains the specified key
     */
    public static function keyExists($key, $array, $caseSensitive = true)
    {
        if ($caseSensitive) {
            // Function `isset` checks key faster but skips `null`, `array_key_exists` handles this case
            // https://secure.php.net/manual/en/function.array-key-exists.php#107786
            return isset($array[$key]) || array_key_exists($key, $array);
        }

        foreach (array_keys($array) as $k) {
            if (strcasecmp($key, $k) === 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * Sorts an array of objects or arrays (with the same structure) by one or several keys.
     * @param array $array the array to be sorted. The array will be modified after calling this method.
     * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array
     * elements, a property name of the objects, or an anonymous function returning the values for comparison
     * purpose. The anonymous function signature should be: `function($item)`.
     * To sort by multiple keys, provide an array of keys here.
     * @param int|array $direction the sorting direction. It can be either `SORT_ASC` or `SORT_DESC`.
     * When sorting by multiple keys with different sorting directions, use an array of sorting directions.
     * @param int|array $sortFlag the PHP sort flag. Valid values include
     * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, `SORT_LOCALE_STRING`, `SORT_NATURAL` and `SORT_FLAG_CASE`.
     * Please refer to [PHP manual](https://secure.php.net/manual/en/function.sort.php)
     * for more details. When sorting by multiple keys with different sort flags, use an array of sort flags.
     * @throws InvalidArgumentException if the $direction or $sortFlag parameters do not have
     * correct number of elements as that of $key.
     */
    public static function multisort(&$array, $key, $direction = SORT_ASC, $sortFlag = SORT_REGULAR)
    {
        $keys = is_array($key) ? $key : [$key];
        if (empty($keys) || empty($array)) {
            return $array;
        }
        $n = count($keys);
        if (is_scalar($direction)) {
            $direction = array_fill(0, $n, $direction);
        } elseif (count($direction) !== $n) {
            throw new InvalidArgumentException('The length of $direction parameter must be the same as that of $keys.');
        }
        if (is_scalar($sortFlag)) {
            $sortFlag = array_fill(0, $n, $sortFlag);
        } elseif (count($sortFlag) !== $n) {
            throw new InvalidArgumentException('The length of $sortFlag parameter must be the same as that of $keys.');
        }
        $args = [];
        foreach ($keys as $i => $k) {
            $flag = $sortFlag[$i];
            $args[] = static::getColumn($array, $k);
            $args[] = $direction[$i];
            $args[] = $flag;
        }

        // This fix is used for cases when main sorting specified by columns has equal values
        // Without it it will lead to Fatal Error: Nesting level too deep - recursive dependency?
        $args[] = range(1, count($array));
        $args[] = SORT_ASC;
        $args[] = SORT_NUMERIC;

        $args[] = &$array;
        call_user_func_array('array_multisort', $args);

        return $array;
    }


    /**
     * 是否是关联数组
     * Returns a value indicating whether the given array is an associative array.
     *
     * An array is associative if all its keys are strings. If `$allStrings` is false,
     * then an array will be treated as associative if at least one of its keys is a string.
     *
     * Note that an empty array will NOT be considered associative.
     *
     * @param array $array the array being checked
     * @param bool $allStrings whether the array keys must be all strings in order for
     * the array to be treated as associative.
     * @return bool whether the array is associative
     */
    public static function isAssociative($array, $allStrings = true)
    {
        if (!is_array($array) || empty($array)) {
            return false;
        }

        if ($allStrings) {
            foreach ($array as $key => $value) {
                if (!is_string($key)) {
                    return false;
                }
            }

            return true;
        }

        foreach ($array as $key => $value) {
            if (is_string($key)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 是否是非关联数组
     * Returns a value indicating whether the given array is an indexed array.
     *
     * An array is indexed if all its keys are integers. If `$consecutive` is true,
     * then the array keys must be a consecutive sequence starting from 0.
     *
     * Note that an empty array will be considered indexed.
     *
     * @param array $array the array being checked
     * @param bool $consecutive whether the array keys must be a consecutive sequence
     * in order for the array to be treated as indexed.
     * @return bool whether the array is indexed
     */
    public static function isIndexed($array, $consecutive = false)
    {
        if (!is_array($array)) {
            return false;
        }

        if (empty($array)) {
            return true;
        }

        if ($consecutive) {
            return array_keys($array) === range(0, count($array) - 1);
        }

        foreach ($array as $key => $value) {
            if (!is_int($key)) {
                return false;
            }
        }

        return true;
    }


    /**
     * Check whether an array or [[\Traversable]] contains an element.
     *
     * This method does the same as the PHP function [in_array()](https://secure.php.net/manual/en/function.in-array.php)
     * but additionally works for objects that implement the [[\Traversable]] interface.
     * @param mixed $needle The value to look for.
     * @param array|\Traversable $haystack The set of values to search.
     * @param bool $strict Whether to enable strict (`===`) comparison.
     * @return bool `true` if `$needle` was found in `$haystack`, `false` otherwise.
     * @throws InvalidArgumentException if `$haystack` is neither traversable nor an array.
     * @see https://secure.php.net/manual/en/function.in-array.php
     * @since 2.0.7
     */
    public static function isIn($needle, $haystack, $strict = false)
    {
        if ($haystack instanceof \Traversable) {
            foreach ($haystack as $value) {
                if ($needle == $value && (!$strict || $needle === $value)) {
                    return true;
                }
            }
        } elseif (is_array($haystack)) {
            return in_array($needle, $haystack, $strict);
        } else {
            throw new InvalidArgumentException('Argument $haystack must be an array or implement Traversable');
        }

        return false;
    }


    /**
     * isIn() 函数的升级版，$needles是数组，此函数会检查每个$needles元素是否都在$hystack中存在
     * Checks whether an array or [[\Traversable]] is a subset of another array or [[\Traversable]].
     *
     * This method will return `true`, if all elements of `$needles` are contained in
     * `$haystack`. If at least one element is missing, `false` will be returned.
     * @param array|\Traversable $needles The values that must **all** be in `$haystack`.
     * @param array|\Traversable $haystack The set of value to search.
     * @param bool $strict Whether to enable strict (`===`) comparison.
     * @return bool `true` if `$needles` is a subset of `$haystack`, `false` otherwise.
     * @throws InvalidArgumentException if `$haystack` or `$needles` is neither traversable nor an array.
     * @since 2.0.7
     */
    public static function isSubset($needles, $haystack, $strict = false)
    {
        if (is_array($needles) || $needles instanceof \Traversable) {
            foreach ($needles as $needle) {
                if (!static::isIn($needle, $haystack, $strict)) {
                    return false;
                }
            }

            return true;
        }

        throw new InvalidArgumentException('Argument $needles must be an array or implement Traversable');
    }


    /**
     * Filters array according to rules specified.
     *
     * For example:
     *
     * ```php
     * $array = [
     *     'A' => [1, 2],
     *     'B' => [
     *         'C' => 1,
     *         'D' => 2,
     *     ],
     *     'E' => 1,
     * ];
     *
     * $result = self::filter($array, ['A']);
     * // $result will be:
     * // [
     * //     'A' => [1, 2],
     * // ]
     *
     * $result = self::filter($array, ['A', 'B.C']);
     * // $result will be:
     * // [
     * //     'A' => [1, 2],
     * //     'B' => ['C' => 1],
     * // ]
     *
     * $result = self::filter($array, ['B', '!B.C']);
     * // $result will be:
     * // [
     * //     'B' => ['D' => 2],
     * // ]
     * ```
     *
     * @param array $array Source array
     * @param array $filters Rules that define array keys which should be left or removed from results.
     * Each rule is:
     * - `var` - `$array['var']` will be left in result.
     * - `var.key` = only `$array['var']['key'] will be left in result.
     * - `!var.key` = `$array['var']['key'] will be removed from result.
     * @return array Filtered array
     * @since 2.0.9
     */
    public static function filterByKey($array, $filters)
    {
        $result = [];
        $forbiddenVars = [];

        foreach ($filters as $var) {
            $keys = explode('.', $var);
            $globalKey = $keys[0];
            $localKey = isset($keys[1]) ? $keys[1] : null;

            if ($globalKey[0] === '!') {
                $forbiddenVars[] = [
                    substr($globalKey, 1),
                    $localKey,
                ];
                continue;
            }

            if (!array_key_exists($globalKey, $array)) {
                continue;
            }
            if ($localKey === null) {
                $result[$globalKey] = $array[$globalKey];
                continue;
            }
            if (!isset($array[$globalKey][$localKey])) {
                continue;
            }
            if (!array_key_exists($globalKey, $result)) {
                $result[$globalKey] = [];
            }
            $result[$globalKey][$localKey] = $array[$globalKey][$localKey];
        }

        foreach ($forbiddenVars as $var) {
            list($globalKey, $localKey) = $var;
            if (array_key_exists($globalKey, $result)) {
                unset($result[$globalKey][$localKey]);
            }
        }

        return $result;
    }

    /**
     * 根据值来过滤数据
     *
     * $arr = [
     *      ['key1' => 'val1'],
     *      ['key1' => 'val1.1'],
     *      ['key1' => 'val1.2'],
     *      ['key2' => 'val2'],
     *      ['key3' => 'val3'],
     * ]
     *
     * self::filterByValue($arr, ['key1' => 'val1'])
     * 结果为：
     * [
     *      ['key1' => 'val1'],
     * ]
     *
     * 通过数组过滤，将返回数组中所有符合的元素
     * self::filterByValue($arr, ['key1' => ['val1', 'val1.1']])
     * 结果为：
     * [
     *      ['key1' => 'val1'],
     *      ['key1' => 'val1.1'],
     * ]
     *
     * 也可以自定义匹配逻辑：
     * self::filterByValue($arr, ['or' => ['key1' => 'val1', 'key2' => 'val2']])
     * 结果为：
     * [
     *      ['key1' => 'val1'],
     *      ['key2' => 'val2'],
     * ]
     *
     * self::filterByValue($arr, ['or' => ['key1' => 'val1', 'key2' => 'val2'], 'and' => ['key1' => 'val1']])
     * [
     *      ['key1' => 'val1'],
     *      ['key1' => 'val1.1'],
     *      ['key1' => 'val1.2'],
     * ]
     *
     * 也可以模糊匹配：
     * self::filterByValue($arr, ['key1' => 'val'], false, true) // true表示开启模糊匹配
     * 结果为：
     * [
     *      ['key1' => 'val1'],
     * ]
     *
     * @param $array
     * @param $filters
     * @param bool $strict // 完全匹配，包括数据类型的对比
     * @param bool $like // 是否开启模糊匹配，只有在$strict=false的时候生效
     * @return array
     */
    public static function filterByValue($array, $filters, $strict = true, $like = false)
    {
        $result = array_filter($array, function ($item) use ($filters, $strict, $like) {
            if (self::keyExists('or', $filters) || self::keyExists('and', $filters)) {
                // ['and' => ['id' => 1], 'or' => ['town_code' => '330881121000', 'vill_code' => '330881005009']]
                $bool = true;
                $boolAnd = true;
                $boolOr = !self::keyExists('or', $filters);
                foreach ($filters as $action => $filter) {
                    if ($action == 'and') {
                        foreach ($filter as $key => $value) {
                            if (is_array($value)) {
                                $boolArr = false;
                                foreach ($value as $val) {
                                    $boolArr = $boolArr || (self::keyExists($key, $item) ? self::compare($item[$key], $val, $strict, $like) : true);
                                }
                                $boolAnd = $boolAnd && $boolArr;
                            } else {
                                $boolAnd = $boolAnd && (self::keyExists($key, $item) ? self::compare($item[$key], $value, $strict, $like) : true);
                            }

                        }

                    } else {
                        foreach ($filter as $key => $value) {
                            if (is_array($value)) {
                                $boolArr = false;
                                foreach ($value as $val) {
                                    $boolArr = $boolArr || (self::keyExists($key, $item) ? self::compare($item[$key], $val, $strict, $like) : true);
                                }
                                $boolOr = $boolOr || $boolArr;
                            } else {
                                $boolOr = $boolOr || (self::keyExists($key, $item) ? self::compare($item[$key], $value, $strict, $like) : true);
                            }

                        }
                    }
                }

                $bool = $bool && $boolAnd && $boolOr;
            } else {
                // ['id' => [1, 2]]
                $bool = true;
                foreach ($filters as $key => $value) {
                    if (is_array($value)) {
                        $bool = false;
                        foreach ($value as $val) {
                            $bool = $bool || (self::keyExists($key, $item) ? self::compare($item[$key], $val, $strict, $like) : true);
                        }
                    } else {
                        $bool = $bool && (self::keyExists($key, $item) ? self::compare($item[$key], $value, $strict, $like) : true);
                    }
                }
            }
            return $bool;
        });

        return array_values($result);
    }

    /**
     * 字符串对比
     * @param string $needle
     * @param string $haystack
     * @param bool $strict
     * @param false $like
     * @return bool
     */
    private static function compare($haystack, $needle, $strict = true, $like = false)
    {
        if ($strict) {
            $result = $needle === $haystack;
        } else {
            if ($like) {
                // 空字符串是所有字符串的子串
                if ($needle == '') return true;
                $result = strpos($haystack, $needle) !== false;
            } else {
                $result = $needle == $haystack;
            }
        }

        return $result;
    }


    /**
     * Change the key of array
     *
     * For example:
     *
     * ```php
     * $array = [
     *     'A' => [1, 2],
     *     'B' => [
     *         'C' => 1,
     *         'D' => 2,
     *     ],
     *     'E' => 1,
     * ];
     *
     * $result = self::setKeys($array, ['A'=>'AA']);
     * // $result will be:
     * $array = [
     *     'AA' => [1, 2],
     *     'B' => [
     *         'C' => 1,
     *         'D' => 2,
     *     ],
     *     'E' => 1,
     * ];
     *
     * ```
     *
     * 也可以实现对索引数组进行修改
     * $arr = [
     *      ['key1' => 'val1']
     * ]
     *
     * self::setKeys($arr, ['key1' => 'key2'])
     *
     * @param array $array Source array
     * @param array $keyMap
     * @return array keys set array
     * @since 2.0.9
     */
    public static function setKeys($array, $keyMap = [])
    {
        $result = [];
        if (self::isIndexed($array)) {
            foreach ($array as $key => $item) {
                $result[$key] = self::setKeys($item, $keyMap);
            }
        } else {
            foreach ($array as $key => $value) {
                if (isset($keyMap[$key])) {
                    $result[$keyMap[$key]] = $value;
                } else {
                    $result[$key] = $value;
                }
            }
        }
        return $result;
    }


    /**
     * Set all values to string
     *
     * For example:
     *
     * ```php
     * $array = [
     *     'age' => 1,
     *     'ids' => [
     *         'key1' => 1,
     *         'key2' => 2,
     *         'key3' => null,
     *     ],
     *     'children' => 0,
     * ];
     *
     * self::setAllValuesToString($array);
     * 结果为：
     * [
     *     'age' => '1',
     *     'ids' => [
     *         'key1' => '1',
     *         'key2' => '2',
     *         'key3' => '',
     *     ],
     *     'children' => '0',
     * ];
     *
     * @param array $array Source array
     * @return array $result
     */
    public static function setAllValuesToString($array)
    {
        $result = [];
        foreach ($array as $key => $item) {
            if (is_array($item)) {
                $result[$key] = self::setAllValuesToString($item);
            } else {
                $result[$key] = strval($item);
            }
        }
        return $result;
    }

    /**
     * 选取指定的字段
     *
     * example:
     * $arr = [
     *      ['col1' => 'val1', 'col2' => 'val2'],
     *      ['col1' => 'val1', 'col2' => 'val2']
     * ]
     *
     * self::getColumns($arr, ['col1'])
     *
     * result:
     * [
     *      ['col1' => 'val1'],
     *      ['col1' => 'val1'],
     * ]
     * @param $array
     * @param array $columns
     * @return array
     */
    public static function getColumns(array $array, array $columns = []): array
    {
        return array_map(function ($item) use ($columns) {
            $result = [];
            foreach ($columns as $column) {
                if (self::keyExists($column, $item)) {
                    $result[$column] = $item[$column];
                }
            }
            return $result;
        }, $array);
    }

    /**
     * 给数组新增字段
     * example:
     * $arr = [
     *      ['col1' => 'val1', 'col2' => 'val2'],
     *      ['col1' => 'val1', 'col2' => 'val2']
     * ]
     *
     * self::addColumns($arr, ['col3' => 'val3'])
     *
     * result:
     * [
     *      ['col1' => 'val1', 'col2' => 'val2', 'col3' => 'val3'],
     *      ['col1' => 'val1', 'col2' => 'val2', 'col3' => 'val3'],
     * ]
     *
     * 也可根据条件进行插入
     *
     * example:
     * $arr = [
     *      ['col1' => 'val1', 'col2' => 'val2'],
     *      ['col1' => 'val1.1', 'col2' => 'val2.1'],
     * ]
     *
     * self::addColumns($arr, ['col3' => 'val3'], ['col1' => '1.1'])
     *
     * result:
     * [
     *      ['col1' => 'val1', 'col2' => 'val2'],
     *      ['col1' => 'val1.1', 'col2' => 'val2.1', 'col3' => 'val3'],
     * ]
     *
     * @param array $array
     * @param array $columns
     * @param bool $cover // 是否覆盖原数组中相同key的值
     * @param array $where
     * @param bool $strict
     * @param bool $like
     * @return array
     */
    public static function addColumns(array $array, array $columns = [], bool $cover = true, array $where = [], bool $strict = false, bool $like = true): array
    {
        return array_map(function ($item) use ($columns, $where, $strict, $like, $cover) {
            if (empty($where)) {
                $item = $cover ? array_merge($item, $columns) : array_merge($columns, $item);
            } else {
                $filter = self::filterByValue([$item], $where, $strict, $like);
                if ($filter) {
                    $item = $cover ? array_merge($item, $columns) : array_merge($columns, $item);
                }
            }
            return $item;
        }, $array);
    }

    /**
     * 行转列
     *
     * For example:
     *
     * ```php
     * $array = [
     *      [
     *          'day' => '2023-02-11',
     *          'fruit' => 'apple',
     *          'price' => 8.1,
     *      ],
     *      [
     *          'day' => '2023-02-11',
     *          'fruit' => 'orange',
     *          'price' => 5.1,
     *      ],
     *      [
     *          'day' => '2023-02-10',
     *          'fruit' => 'apple',
     *          'price' => 8.2,
     *      ],
     *      [
     *          'day' => '2023-02-10',
     *          'fruit' => 'orange',
     *          'price' => 5.2,
     *      ],
     * ];
     *
     * self::rowToColumn($array, ['day'], ['fruit' => 'price']);
     *
     * result：
     * [
     *     ['day' => '2023-02-11', 'apple' => 8.1, 'orange' => 5.1],
     *     ['day' => '2023-02-10', 'apple' => 8.2, 'orange' => 5.2],
     * ];
     * @param array $array
     * @param array $index
     * @param array $maps
     * @return array
     */
    public static function rowToColumn($array = [], $index = [], $maps = [])
    {
        $result = [];
        foreach ($array as $item) {
            $key = implode(',', array_map(function ($column) use ($item) {
                return $item[$column];
            }, $index));
            foreach ($index as $column) {
                $result[$key][$column] = $item[$column];
            }
            foreach ($maps as $from => $to) {
                $result[$key][$item[$from]] = $item[$to];
            }
        }
        return $result;
    }


    /**
     * 按照条件删除
     *
     * For example:
     *
     * ```php
     * $array = [
     *      ['day' => '2023-02-11','fruit' => 'apple','price' => 8.1,],
     *      ['day' => '2023-02-12','fruit' => 'apple','price' => 8.1,],
     *      ['day' => '2023-02-13','fruit' => 'apple','price' => 8.1,],
     * ];
     *
     * self::remove(['day' => '2023-02-11']);
     *
     * result：
     * [
     *      ['day' => '2023-02-12','fruit' => 'apple','price' => 8.1,],
     *      ['day' => '2023-02-13','fruit' => 'apple','price' => 8.1,],
     * ];
     *
     * @param array $array
     * @param array $where
     * @param false $like
     * @return array
     */
    public function removeByWhere($array = [], $where = [], $like = false): array
    {
        foreach ($array as $key => $item) {
            $check = false;
            foreach ($where as $k => $v) {
                if (!iArrayHelper::keyExists($k, $item)) {
                    continue;
                }
                if ($like) {
                    // 模糊匹配
                    $check = strpos($item[$k], $v) !== false;
                } else {
                    $check = $item[$k] == $v;
                }
            }
            if ($check) {
                unset($array[$key]);
            }
        }
        return $array;
    }


    /**
     * trim
     * @param array $array // 数据源
     * @param string[] $columns // 需要trim的字段，默认全部转换
     * @param false $recursive // 是否递归处理
     * @return array
     */
    public static function trim($array = [], $columns = ['*'], $recursive = false)
    {
        $result = [];
        foreach ($array as $key => $item) {
            if (is_array($item)) {
                if ($recursive) {
                    $result[$key] = self::trim($item);
                } else {
                    $result[$key] = $item;
                }
            } else {
                $result[$key] = (in_array($key, $columns) || in_array('*', $columns)) ? trim($item) : $item;
            }
        }
        return $result;
    }
}
